15 设计模式——访问者模式

返回设计模式博客目录

介绍


访问者(Visitor)模式:封装某些作用于某种数据结构中各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作

访问者模式是一种将数据操作与数据结构分离的设计模式,它是 23 种设计模式中最复杂的一个,但是它的使用频率并不高,正如《设计模式》的作者 GOF 对访问者模式的描述:大多数情况下,并不需要使用访问者模式,但是当你一旦需要使用它时,那你就是真的需要它了。

访问者模式的基本想法是,软件系统中拥有一个由许多对象构成的、比较稳定的对象结构,这些对象的类都拥有一个 accept 方法用来接受访问者对象的访问。访问者是一个接口,它拥有一个 visit 方法,这个方法对访问到的对象结构中不同类型的元素做出不同的处理。在对象结构的每一次访问过程中,我们遍历整个对象结构,对每一个元素都实施 accept 方法,在每一个元素的 accept 方法中会调用访问者的 visit 方法,从而使访问者得以处理对象结构的每一个元素,我们可以针对对象结构设计不同的访问者类来完成不同的操作,达到区别对待的效果。

优点

  • 各角色职责分离,符合单一职责原则。
  • 具有优秀的扩展性。
  • 使得数据结构和作用于结构上的操作解耦,使得操作集合可以独立变化。
  • 灵活性。

缺点

  • 具体元素对访问者公布细节,违反了迪米特原则。
  • 违反了依赖倒置原则,为了达到“区别对待”而依赖了具体类,没有依赖抽象。

使用场景

  • 对象结构比较稳定,但经常需要在此对象结构上定义新的操作。
  • 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。

结构与实现


模式包含以下主要角色。

  • Visitor(抽象访问者):接口或者抽象类,为每一个元素(Element)声明一个访问的方法。
  • ConcreteVisitor(具体访问者):实现抽象访问者中的方法,即对每一个元素都有其具体的访问行为。
  • Element(抽象元素):接口或者抽象类,定义一个accept方法,能够接受访问者(Visitor)的访问。
  • ConcreteElementA、ConcreteElementB(具体元素):实现抽象元素中的accept方法,通常是调用访问者提供的访问该元素的方法。
  • Object Structure(对象结构):是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由 List、Set、Map 等聚合类实现。
  • Client(客户端类):即要使用访问者模式的地方。

其结构图如下图所示。

访问者模式的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class VisitorPattern {
public static void main(String[] args) {
ObjectStructure os = new ObjectStructure();
os.add(new ConcreteElementA());
os.add(new ConcreteElementB());
Visitor visitor = new ConcreteVisitorA();
os.accept(visitor);
System.out.println("------------------------");
visitor = new ConcreteVisitorB();
os.accept(visitor);
}
}
// 抽象访问者
interface Visitor {
void visit(ConcreteElementA element);
void visit(ConcreteElementB element);
}
// 具体访问者 A 类
class ConcreteVisitorA implements Visitor {
public void visit(ConcreteElementA element) {
System.out.println("具体访问者A访问-->"+element.operationA());
}
public void visit(ConcreteElementB element) {
System.out.println("具体访问者A访问-->"+element.operationB());
}
}
// 具体访问者 B 类
class ConcreteVisitorB implements Visitor {
public void visit(ConcreteElementA element) {
System.out.println("具体访问者B访问-->"+element.operationA());
}
public void visit(ConcreteElementB element) {
System.out.println("具体访问者B访问-->"+element.operationB());
}
}
// 抽象元素类
interface Element {
void accept(Visitor visitor);
}
// 具体元素 A 类
class ConcreteElementA implements Element {
public void accept(Visitor visitor) {
visitor.visit(this);
}
public String operationA() {
return "具体元素A的操作。";
}
}
// 具体元素 B 类
class ConcreteElementB implements Element {
public void accept(Visitor visitor) {
visitor.visit(this);
}
public String operationB() {
return "具体元素B的操作。";
}
}
// 对象结构角色
class ObjectStructure {
private List<Element> list = new ArrayList<Element>();
public void accept(Visitor visitor) {
Iterator<Element> i= list.iterator();
while (i.hasNext()) {
((Element) i.next()).accept(visitor);
}
}
public void add(Element element) {
list.add(element);
}
public void remove(Element element) {
list.remove(element);
}
}

程序的运行结果如下:

1
2
3
4
5
具体访问者A访问-->具体元素A的操作。
具体访问者A访问-->具体元素B的操作。
------------------------
具体访问者B访问-->具体元素A的操作。
具体访问者B访问-->具体元素B的操作。

示例


利用“访问者(Visitor)模式”模拟艺术公司与造币公司的功能。

分析:艺术公司利用“铜”可以设计出铜像,利用“纸”可以画出图画;造币公司利用“铜”可以印出铜币,利用“纸”可以印出纸币。对“铜”和“纸”这两种元素,两个公司的处理方法不同,所以该实例用访问者模式来实现比较适合。

首先,定义一个公司(Company)接口,它是抽象访问者,提供了两个根据纸(Paper)或铜(Cuprum)这两种元素创建作品的方法;再定义艺术公司(ArtCompany)类和造币公司(Mint)类,它们是具体访问者,实现了父接口的方法;然后,定义一个材料(Material)接口,它是抽象元素,提供了 accept(Company visitor)方法来接受访问者(Company)对象访问;再定义纸(Paper)类和铜(Cuprum)类,它们是具体元素类,实现了父接口中的方法;最后,定义一个材料集(SetMaterial)类,它是对象结构角色,拥有保存所有元素的容器 List,并提供让访问者对象遍历容器中的所有元素的 accept(Company visitor)方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 抽象访问者:公司
interface Company {
String create(Paper element);
String create(Cuprum element);
}
// 具体访问者:艺术公司
class ArtCompany implements Company {
public String create(Paper element) {
return "讲学图";
}
public String create(Cuprum element) {
return "朱熹铜像";
}
}
// 具体访问者:造币公司
class Mint implements Company {
public String create(Paper element) {
return "纸币";
}
public String create(Cuprum element) {
return "铜币";
}
}
// 抽象元素:材料
interface Material {
String accept(Company visitor);
}
// 具体元素:纸
class Paper implements Material {
public String accept(Company visitor) {
return (visitor.create(this));
}
}
// 具体元素:铜
class Cuprum implements Material {
public String accept(Company visitor) {
return (visitor.create(this));
}
}
// 对象结构角色:材料集
class SetMaterial {
private List<Material> list = new ArrayList<>();
public String accept(Company visitor) {
Iterator<Material> iterator = list.iterator();
StringBuilder sb = new StringBuilder();
String temp;
while(iterator.hasNext()) {
temp = iterator.next().accept(visitor);
sb.append(temp).append(" ");
}
return sb.toString(); // 返回某公司的作品集
}
public void add(Material element) {
list.add(element);
}
public void remove(Material element) {
list.remove(element);
}
}

ANDROID 源码中的实现


Android 的编译时注解是一种访问者模式。编译时注解的核心原理依赖 APT (Annotation Processing Tools)实现。

编译时注解解析的基本原理是,在某些代码元素上(如类型、函数、字段等)添加注解,在编译时编译器会检查 AbstractProcessor 的子类,并且调用该类型的 process 函数,然后将添加了注解的所有元素都传递到 process 函数中,使得开发人员可以在编译期进行相应的处理。

编写注解处理器的核心是 AnnotationProcessorFactory 和 AnnotationProcessor 两个接口,后者表示的是注解处理器,而前者则是为某些注解类型创建注解处理器的工厂。

对于编译器来说,代码中的元素结构是基本不变的,例如,组成代码的基本元素由包、类、函数、字段、类型参数、变量。JDK 中为这些元素定义了一个基类,也就是 Element 类,它有如下几个子类:

  • PackageElement 包元素,包含了某个包下的信息,可以获取到包名等;
  • TypeElement:类型元素,如某个字段属于某种类型;
  • ExecutableElement:可执行元素,代表了函数类型的元素;
  • VariableElement:变量元素;
  • TypeParameterElement:类型参数元素。

因为注解可以指定作用在哪些元素上,因此,通过上述的抽象来对应这些元素,例如下面这个注解,指定的是只能作用于方法上面,并且这个注解只能保留在 class 文件中(编译时注解):

1
2
3
4
5
@Target(ElementType. METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface Test {
String value();
}

该注解因为只能作用于函数类型,因此,它对应的元素类型就是 ExecutableElement,当我们想通过 APT 处理这个注解时就可以获取目标对象上的 Test 注解,并将所有这些元素转换为 ExecutableElement 元素,以便获取到它们对应的信息。

我们看看元素基类的实现,完整的路径为 javax.lang.model.element.Element。

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Element extends AnnotatedConstruct {
TypeMirror asType();
// 获取元素类型
ElementKind getKind();
// 获取元素修饰符,如 public、static、final 等
Set<Modifier> getModifiers();
// 代码省略
List<? extends Element> getEnclosedElements();
// 接受访问者的访问
<R, P> R accept(ElementVisitor<R, P> v, P p);
}

可以看到 Element 定义了一个代码元素的一些通用接口,其中很显眼的就是 accept 函数,这个函数接收一个 ElementVisitor 和类型为 P 的参数,ElementVisitor 就是访问者类型,而 P 则用于传递一些额外的参数给 visitor。这是一个典型的访问者模式。

ElementVisitor 定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 元素访问者
public interface ElementVisitor<R, P> {
// 访问元素
R visit(Element var1, P var2);
R visit(Element var1);
// 访问包元素
R visitPackage(PackageElement var1, P var2);
// 访问类型元素
R visitType(TypeElement var1, P var2);
// 访问变量元素
R visitVariable(VariableElement var1, P var2);
// 访问可执行元素
R visitExecutable(ExecutableElement var1, P var2);
// 访问参数元素
R visitTypeParameter(TypeParameterElement var1, P var2);
// 访问位置元素,为后续扩展预留的接口
R visitUnknown(Element var1, P var2);
}

当 Visitor 对元素结构进行访问时,就可以针对不同的类型进行不同的处理。例如 SimpleElementVisitor6 就是其中一个访问者,它基本上没做什么操作,直接返回了元素的默认值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class SimpleElementVisitor6<R, P> extends AbstractElementVisitor6<R, P> {
protected final R DEFAULT_VALUE;
protected SimpleElementVisitor6() {
this.DEFAULT_VALUE = null;
}
protected SimpleElementVisitor6(R var1) {
this.DEFAULT_VALUE = var1;
}
protected R defaultAction(Element var1, P var2) {
return this.DEFAULT_VALUE;
}
public R visitPackage(PackageElement var1, P var2) {
return this.defaultAction(var1, var2);
}
public R visitType(TypeElement var1, P var2) {
return this.defaultAction(var1, var2);
}
public R visitVariable(VariableElement var1, P var2) {
return var1.getKind() != ElementKind.RESOURCE_VARIABLE ? this.defaultAction(var1, var2) : this.visitUnknown(var1, var2);
}
public R visitExecutable(ExecutableElement var1, P var2) {
return this.defaultAction(var1, var2);
}
public R visitTypeParameter(TypeParameterElement var1, P var2) {
return this.defaultAction(var1, var2);
}
}

另一个提取元素类型的访问者是 ElementKindVisitor6:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class ElementKindVisitor6<R, P> extends SimpleElementVisitor6<R, P> {
protected ElementKindVisitor6() {
super((Object)null);
}
protected ElementKindVisitor6(R var1) {
super(var1);
}
public R visitPackage(PackageElement var1, P var2) {
assert var1.getKind() == ElementKind.PACKAGE : "Bad kind on PackageElement";
return this.defaultAction(var1, var2);
}
// 访问类型元素,比如类、注解、枚举、接口
public R visitType(TypeElement var1, P var2) {
ElementKind var3 = var1.getKind();
switch(var3) {
case ANNOTATION_TYPE:
return this.visitTypeAsAnnotationType(var1, var2);
case CLASS:
return this.visitTypeAsClass(var1, var2);
case ENUM:
return this.visitTypeAsEnum(var1, var2);
case INTERFACE:
return this.visitTypeAsInterface(var1, var2);
default:
throw new AssertionError("Bad kind " + var3 + " for TypeElement" + var1);
}
}
public R visitTypeAsAnnotationType(TypeElement var1, P var2) {
return this.defaultAction(var1, var2);
}
public R visitTypeAsClass(TypeElement var1, P var2) {
return this.defaultAction(var1, var2);
}
public R visitTypeAsEnum(TypeElement var1, P var2) {
return this.defaultAction(var1, var2);
}
public R visitTypeAsInterface(TypeElement var1, P var2) {
return this.defaultAction(var1, var2);
}
...
}

ElementKindVisitor6 对于不同的类型进行不同的处理,提取各个元素的类型信息,例如,上述代码中对于 Type 类型的元素将分别进行处理,如类、枚举、接口、注解等。

首先,编译器将代码抽象成一个代码元素的树,然后再编译时对整棵树进行遍历访问,每个元素都有一个 accept 函数接受访问者的访问,每个访问者中都有对应的 visit 函数,例如,visitType 函数就是对类型元素的访问,在每个 visit 函数中对不同的类型进行不同的处理,这样就达到了差异处理的效果,同时将数据结构和数据操作分离,使得每个类型的职责单一,易于升级维护。JDK 还特意预留了 visitUnknown 接口来应对 Java 语言后续发展可能添加元素类型的问题,灵活地将访问者模式的缺点优化。